/*
 * Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2006.
 *
 * Licensed under the Aduna BSD-style license.
 */
 
package edu.tju.commons.net;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * Methods related to actions on URIs. The definition of a URI is taken from <a
 * href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396: URI Generic Syntax</a>.
 */
public final class UriUtil {

	private static final String ESCAPE_CHARS = "<>%\"{}|\\^[]`";

	/**
	 * Encodes a string according to RFC 2396. According to this spec, any
	 * characters outside the range 0x20 - 0x7E must be escaped because they are
	 * not printable characters, except for characters in the fragment
	 * identifier. Even within this range a number of characters must be
	 * escaped. This method will perform this escaping.
	 *
	 * @param uri
	 *        The URI to encode.
	 * @return The encoded URI.
	 */
	public static String encodeUri(String uri) {
		StringBuilder result = new StringBuilder();
		encodeUri(uri, result);
		return result.toString();
	}

	/**
	 * Encodes a string according to RFC 2396.
	 *
	 * @param uri
	 *        The URI to encode.
	 * @param buf
	 *        The StringBuilder that the encoded URI will be appended to.
	 * @see #encodeUri(java.lang.String)
	 */
	public static void encodeUri(String uri, StringBuilder buf) {
		int uriLen = uri.length();
		char c;
		int cInt;
		String hexVal;
		for (int i = 0; i < uriLen; i++) {
			c = uri.charAt(i);
			cInt = (int) c;

			if (ESCAPE_CHARS.indexOf(c) >= 0 || cInt <= 0x20) {
				// Escape character
				buf.append('%');
				hexVal = Integer.toHexString(cInt);

				// Ensure use of two characters
				if (hexVal.length() == 1) {
					buf.append('0');
				}

				buf.append(hexVal);
			}
			else {
				buf.append(c);
			}
		}
	}

	/**
	 * Decodes a string according to RFC 2396. According to this spec, any
	 * characters outside the range 0x20 - 0x7E must be escaped because they are
	 * not printable characters, except for any characters in the fragment
	 * identifier. This method will translate any escaped characters back to the
	 * original.
	 *
	 * @param uri
	 *        The URI to decode.
	 * @return The decoded URI.
	 */
	public static String decodeUri(String uri) {
		StringBuilder result = new StringBuilder();
		decodeUri(uri, result);
		return result.toString();
	}

	/**
	 * Decodes a string according to RFC 2396.
	 *
	 * @param uri
	 *        The URI to decode.
	 * @param buf
	 *        The StringBuilder that the decoded URI will be appended to.
	 * @see #decodeUri(java.lang.String)
	 */
	public static void decodeUri(String uri, StringBuilder buf) {
		int percentIdx = uri.indexOf('%');
		int startIdx = 0;

		while (percentIdx != -1 && percentIdx < uri.length() - 3) {
			buf.append(uri.substring(startIdx, percentIdx));

			// The two character following the '%' contain a hexadecimal
			// code for the original character, i.e. '%20'
			String xx = uri.substring(percentIdx + 1, percentIdx + 3);
			
			try {
				buf.append((char) Integer.parseInt(xx, 16));
			}
			catch (NumberFormatException e) {
				// apparently not a number, don't replace it
				buf.append('%');
				buf.append(xx);
			}

			startIdx = percentIdx + 3;

			percentIdx = uri.indexOf('%', startIdx);
		}

		buf.append(uri.substring(startIdx));
	}

	/**
	 * Builds a query string from the provided key-value-pairs. All spaces are
	 * substituted by '+' characters, and all non US-ASCII characters are
	 * escaped to hexadecimal notation (%xx).
	 */
	public static String buildQueryString(Map<String, String> keyValuePairs) {
		StringBuilder result = new StringBuilder();

		Set<Map.Entry<String, String>> entrySet = keyValuePairs.entrySet();
		Iterator<Map.Entry<String, String>> iter = entrySet.iterator();

		while (iter.hasNext()) {
			// Iterate over all key-value pairs
			Map.Entry<String, String> keyValuePair = iter.next();

			String key = keyValuePair.getKey();
			String value = keyValuePair.getValue();

			// Escape both key and value and combine them with an '='
			formUrlEncode(key, result);
			result.append('=');
			formUrlEncode(value, result);

			// If there are more key-value pairs, append an '&'
			if (iter.hasNext()) {
				result.append('&');
			}
		}

		return result.toString();
	}

	/**
	 * Encodes a string according to RFC 1738 : Uniform Resource locators (URL).
	 * According to this spec, any characters outside the range 0x20 - 0x7E must
	 * be escaped because they are not printable characters. Within the range a
	 * number of characters are deemed unsafe or are marked as reserved. In
	 * short: According to the spec only the alphanumerics and the special
	 * characters from <tt>$-_.+!*'(),</tt> can be left unencoded. To be save
	 * this method will encode all characters that are not alphanumerics.
	 */
	public static void formUrlEncode(String s, StringBuilder buf) {
		for (int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			int cInt = (int) c;

			// Only characters in the range 48 - 57 (numbers), 65 - 90 (upper
			// case letters), 97 - 122 (lower case letters) can be left
			// unencoded. The rest needs to be escaped.

			if (cInt >= 48 && cInt <= 57 || cInt >= 65 && cInt <= 90 || cInt >= 97 && cInt <= 122) {
				// alphanumeric character
				buf.append(c);
			}
			else {
				// Escape all non-alphanumerics
				buf.append('%');
				String hexVal = Integer.toHexString(cInt);

				// Ensure use of two characters
				if (hexVal.length() == 1) {
					buf.append('0');
				}

				buf.append(hexVal);
			}
		}
	}

	public static String formUrlEncode(String s) {
		StringBuilder result = new StringBuilder();
		formUrlEncode(s, result);
		return result.toString();
	}

	/**
	 * Get a valid URI representation of the given URL.
	 *
	 * @throws URISyntaxException
	 *         If constructing the URI failed.
	 */
	public static URI urlToUri(URL url) throws URISyntaxException {
		return new URI(encodeUri(url.toExternalForm()));
	}
}
